tiptap-markdown-react 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -15
- package/THIRD_PARTY_LICENSES.md +45 -1
- package/dist/{ReportContent-MgbebxPV.js → ReportContent-B8jIzhy3.js} +8 -8
- package/dist/ReportContent-B8jIzhy3.js.map +1 -0
- package/dist/chart/columnMatching.d.ts +1 -1
- package/dist/chart/types.d.ts +1 -1
- package/dist/commentAnchor/commentMapper.d.ts +1 -1
- package/dist/components/EditorToolbar.d.ts +1 -1
- package/dist/components/ImageAlignTools.d.ts +19 -0
- package/dist/components/ImageUploadView.d.ts +10 -0
- package/dist/components/MarkdownWysiwygEditor.d.ts +47 -1
- package/dist/components/imageUpload.dom.test.d.ts +1 -0
- package/dist/extensions.d.ts +12 -0
- package/dist/{extractToc-BaR6LP1f.js → extractToc-CGrBMGvM.js} +433 -9
- package/dist/extractToc-CGrBMGvM.js.map +1 -0
- package/dist/icons/index.d.ts +9 -0
- package/dist/imageAlign.d.ts +29 -0
- package/dist/imageAlign.dom.test.d.ts +1 -0
- package/dist/imageCaption.d.ts +12 -0
- package/dist/imageMarkdown.d.ts +86 -0
- package/dist/imageResize.dom.test.d.ts +1 -0
- package/dist/imageUpload.d.ts +63 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +1355 -245
- package/dist/index.js.map +1 -1
- package/dist/labels.d.ts +52 -0
- package/dist/paragraphMarkdown.d.ts +19 -0
- package/dist/paragraphStyles.d.ts +38 -0
- package/dist/paragraphStyles.dom.test.d.ts +1 -0
- package/dist/reader.js +2 -2
- package/dist/resetImageSize.d.ts +12 -0
- package/dist/{scrollToTocHeading-BTQWajGD.js → scrollToTocHeading-B2QmL4wi.js} +2 -2
- package/dist/{scrollToTocHeading-BTQWajGD.js.map → scrollToTocHeading-B2QmL4wi.js.map} +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +4 -4
- package/dist/style-inline.css +671 -140
- package/dist/style.css +671 -140
- package/package.json +5 -2
- package/dist/ReportContent-MgbebxPV.js.map +0 -1
- package/dist/extractToc-BaR6LP1f.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# tiptap-markdown-react
|
|
2
2
|
|
|
3
|
-
A batteries-included, self-styled **Markdown WYSIWYG editor + reader** suite built on [Tiptap v3](https://tiptap.dev). Markdown in, markdown out — plus a table of contents, a client preview, and a server-side (RSC/SSR) renderer for SEO-friendly reading pages. No
|
|
3
|
+
A batteries-included, self-styled **Markdown WYSIWYG editor + reader** suite built on [Tiptap v3](https://tiptap.dev). Markdown in, markdown out — plus a table of contents, a client preview, and a server-side (RSC/SSR) renderer for SEO-friendly reading pages. No UI framework dependency, no icon library; themeable via CSS variables.
|
|
4
4
|
|
|
5
5
|
**[Live demo →](https://catsinjune.github.io/tiptap-markdown-react/)** — the docs site with runnable editor demos (source in [`site/`](./site)). Auto-deployed from `main`.
|
|
6
6
|
|
|
7
7
|
- **Markdown-first**: content goes in and comes out as markdown (`getMarkdown()`), with `getHTML()` / `getJSON()` also exposed.
|
|
8
8
|
- **Equations**: toolbar inserts inline / block math (KaTeX). Markdown round-trip uses `$$…$$` (inline) and newline-wrapped `$$` (block). Typing `$` / `$$` stays as text so dollar amounts are safe.
|
|
9
|
-
- **Charts**:
|
|
10
|
-
- **Own opinionated UI**: toolbar, color palette, code block, and table of contents ship styled out of the box.
|
|
9
|
+
- **Charts**: LLM-friendly data charts (`<!-- {"chartType":…} -->` + GFM table). Chart.js renders in editor / preview / reader (SSR placeholder → client hydrate). MVP: line, bar, column, pie, donut, area.
|
|
10
|
+
- **Own opinionated UI**: toolbar, color palette, code block, and table of contents ship styled out of the box. Dropdowns/popovers use [Radix](https://www.radix-ui.com/) primitives; icons are inline SVG.
|
|
11
11
|
- **Editor + Preview + Static reader**: edit, live client-side preview, and a pure `renderReportHtml()` for server rendering (Next.js Server Components / ISR). Reading pages that only hydrate footnotes should import `tiptap-markdown-react/reader` so they do not load `TableKit`.
|
|
12
12
|
- **Table of contents**: stable, shareable slug anchors that match between the editor preview and the published reading page.
|
|
13
13
|
- **Themeable**: colors and fonts are exposed as `--tmr-*` CSS variables.
|
|
@@ -66,19 +66,21 @@ export function Composer() {
|
|
|
66
66
|
return (
|
|
67
67
|
<div>
|
|
68
68
|
{editor && (
|
|
69
|
-
<EditorToolbar
|
|
70
|
-
editor={editor}
|
|
71
|
-
onImageUpload={async (file) => {
|
|
72
|
-
const url = await uploadToYourStorage(file); // return a public URL
|
|
73
|
-
return url;
|
|
74
|
-
}}
|
|
75
|
-
onError={(err) => console.error(err)}
|
|
76
|
-
/>
|
|
69
|
+
<EditorToolbar editor={editor} onError={(err) => console.error(err)} />
|
|
77
70
|
)}
|
|
78
71
|
<MarkdownWysiwygEditor
|
|
79
72
|
ref={ref}
|
|
80
73
|
initialMarkdown={'# Hello\n\nStart writing…'}
|
|
81
74
|
placeholder="Write something…"
|
|
75
|
+
// The toolbar's image button now inserts a drop/click upload block
|
|
76
|
+
// (official ImageUploadNode interaction); it swaps itself for the
|
|
77
|
+
// image when the upload finishes — see "Image upload" below.
|
|
78
|
+
imageUpload={{
|
|
79
|
+
upload: async (file, onProgress, signal) => {
|
|
80
|
+
return uploadToYourStorage(file, onProgress, signal); // public URL
|
|
81
|
+
},
|
|
82
|
+
maxSize: 5 * 1024 * 1024,
|
|
83
|
+
}}
|
|
82
84
|
onEditorReady={setEditor}
|
|
83
85
|
/>
|
|
84
86
|
<button onClick={() => console.log(ref.current?.getMarkdown())}>
|
|
@@ -161,7 +163,7 @@ Do not also import `tiptap-markdown-react` (the editor entry) from that same cli
|
|
|
161
163
|
|
|
162
164
|
### 3b. Charts (comment + table)
|
|
163
165
|
|
|
164
|
-
Author / LLM form
|
|
166
|
+
Author / LLM form:
|
|
165
167
|
|
|
166
168
|
```markdown
|
|
167
169
|
<!-- {"chartType": "line", "x": "date", "y": "close", "title": "Price"} -->
|
|
@@ -287,11 +289,13 @@ Override any of these CSS variables on an ancestor (e.g. `:root` or the editor c
|
|
|
287
289
|
| `onTocChange` | `(items: TocItem[]) => void` | Fires when headings change. |
|
|
288
290
|
| `markdownPaste` | `boolean` | Auto-detect markdown in plain-text paste and convert it (Shift+paste keeps plain text). Default `true`. Init-only. |
|
|
289
291
|
| `markdownFileDrop` | `boolean` | Drop or paste `.md` / `.markdown` files into the editor to insert their parsed content. Default `true`. Init-only. |
|
|
292
|
+
| `imageUpload` | `ImageUploadConfig` | Official `ImageUploadNode`-style upload (see [Image upload](#image-upload)). The toolbar's image button inserts a placeholder block you can drop a file on or click to pick; progress shows in place and the block is swapped for the image when every file finishes. `upload(file, onProgress?, signal?) => Promise<url>` is required; `accept` (`image/*`), `limit` (1), `maxSize` (0 = unlimited), `onError`, `onSuccess`, `labels` are optional. The block never reaches the saved markdown. Init-only. |
|
|
293
|
+
| `imageResize` | `boolean \| ImageResizeOptions` | Resizable images (default `true`, see [Image resize](#image-resize)). Drag a corner handle to resize; the size lands in the `width`/`height` node attributes and round-trips through markdown as `<img …>` (untouched images stay ``; `resetImageSize(editor)` clears a size). `false` disables; an object passes through to the official `resize` config (`directions`, `minWidth`, `minHeight`, `alwaysPreserveAspectRatio`). Init-only. |
|
|
290
294
|
| `extraExtensions` | `AnyExtension[]` | Extra Tiptap extensions to register. |
|
|
291
295
|
| `codeBlockLabels` | `Partial<CodeBlockLabels>` | Localize the code block UI. |
|
|
292
296
|
| `findReplace` | `boolean` | Enable find & replace (default `true`): registers the official `@tiptap/extension-find-and-replace` and renders the floating bar. |
|
|
293
297
|
| `findBar` | `boolean` | Render the floating bar inside the editor (default: same as `findReplace`). `false` hands **placement to the host** — the extension stays registered, the editor renders no bar, and you render `<FindReplaceBar editor={editor} />` wherever you want; `findShortcut` and `handle.openFind/closeFind` go quiet with it. |
|
|
294
|
-
| `findBarContainer` | `HTMLElement \| (() => HTMLElement \| null)` | Mount the editor's bar into a container you own (the
|
|
298
|
+
| `findBarContainer` | `HTMLElement \| (() => HTMLElement \| null)` | Mount the editor's bar into a container you own (the popup-container pattern): the library keeps open state and the shortcut, only the mount point changes. Use it when an `overflow: hidden` ancestor would clip the bar, or to park it in your own header / sidebar. Positioning is then yours (no absolute positioning is added), and if the container sits outside your themed subtree, bring `--tmr-*` along. Falls back to the in-editor bar when it resolves to `null`. |
|
|
295
299
|
| `findBarOffset` | `{ top?, right?, bottom?, left? } \| number = px` | Where the library-rendered bar sits inside its positioning context. Default `{ top: 4, right: 4 }` (top-right); pass e.g. `{ bottom: 8, left: 8 }` to dock it elsewhere. The context is the editor, or your `findBarContainer` element when you supply one — the library adds `position: relative` to that container if it is `static` (otherwise the bar would anchor to some unexpected ancestor). |
|
|
296
300
|
| `findShortcut` | `boolean` | Take over <kbd>Cmd/Ctrl</kbd>+<kbd>F</kbd> while the editor has focus (default `true`; bound only when the editor owns a bar). `false` keeps the browser's native find — wire your own entry with `handle.openFind()`. |
|
|
297
301
|
| `findLabels` | `Partial<FindLabels>` | Localize the find & replace bar. |
|
|
@@ -406,14 +410,87 @@ Three ways to get markdown into the editor, all built in:
|
|
|
406
410
|
|
|
407
411
|
The underlying extensions `MarkdownPaste` / `MarkdownFileDrop` (and the `looksLikeMarkdown` heuristic) are exported for custom pipelines.
|
|
408
412
|
|
|
413
|
+
#### Image upload
|
|
414
|
+
|
|
415
|
+
Configure `imageUpload` on the editor and the toolbar's image button stops opening the file picker directly. Instead it **inserts a placeholder block** in the document — the same interaction as the official `ImageUploadNode` UI component: drop a file on the block or click to pick one, the block lists each file with its size and a live progress bar, and once every queued file finishes it replaces itself with the matching image node(s) at the same position.
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
<MarkdownWysiwygEditor
|
|
419
|
+
imageUpload={{
|
|
420
|
+
upload: async (file, onProgress, signal) => {
|
|
421
|
+
return yourApi.upload(file, { onProgress, signal }); // public URL (or data URL)
|
|
422
|
+
},
|
|
423
|
+
accept: 'image/*', // default
|
|
424
|
+
limit: 3, // queue up to 3 per block; default 1
|
|
425
|
+
maxSize: 5 * 1024 * 1024, // per-file cap in bytes; 0 = unlimited (default 0)
|
|
426
|
+
onError: (err) => console.error(err),
|
|
427
|
+
}}
|
|
428
|
+
/>
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
- **Markdown stays clean** — the block is an atom node whose `renderMarkdown` returns `''`, so an in-flight upload can never leak into `getMarkdown()` / autosave; images appear only after the swap. On success `alt` / `title` are set to the file name (extension stripped).
|
|
432
|
+
- **Errors** — over-size / over-limit / failed uploads call `onError(error)`; the failed row stays in the block. Remove it (or Clear all) and try again.
|
|
433
|
+
- **Abort** — the `upload` callback receives an `AbortSignal` that fires when a row is removed, the queue is cleared, the block is deleted, or the editor unmounts. Hand it to `fetch` so abandoned uploads stop.
|
|
434
|
+
- **Keyboard** — <kbd>Cmd/Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> inserts a block (on Windows that combo also opens DevTools — the browser wins, same as the official component); <kbd>Enter</kbd> on a selected block opens the picker.
|
|
435
|
+
- The block's text comes from the `imageUpload.labels` (`ImageUploadLabels`); the `ImageUploadNode` extension and `hasImageUpload(editor)` are exported for hand-built pipelines.
|
|
436
|
+
- The legacy toolbar prop `onImageUpload` still works when the extension is not registered — see the `<EditorToolbar>` table.
|
|
437
|
+
|
|
438
|
+
#### Paragraph styles: alignment, indent, line height
|
|
439
|
+
|
|
440
|
+
Three dropdowns sit next to the font-size control: **alignment** (left / center / right / justify — the free official `@tiptap/extension-text-align`), **indent** (increase / decrease, one 2em step per level, max 8) and **line height** (default / 1 / 1.15 / 1.5 / 2 / 2.5 / 3). They apply to paragraphs and headings; commands are `setTextAlign` (official) plus `setIndent` / `increaseIndent` / `decreaseIndent` / `setLineHeight` from the exported `ParagraphStyles` extension.
|
|
441
|
+
|
|
442
|
+
Markdown has no paragraph-style syntax, so a styled paragraph serializes to inline HTML — the same bus as resized images, with the same "only when used" rule:
|
|
443
|
+
|
|
444
|
+
```html
|
|
445
|
+
<p style="text-align: center; margin-left: 2em; line-height: 1.5">…</p>
|
|
446
|
+
<h2 style="text-align: right">…</h2>
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Untouched paragraphs stay native markdown. The editor, preview and server renderer share one tokenizer (it only claims `<p>` / `<hN>` carrying styles this library knows), so styles round-trip everywhere — including SSR. On other renderers (GitHub etc.) the `style` is sanitized away while the text renders normally: a decoration is lost, not content.
|
|
450
|
+
|
|
451
|
+
#### Image resize
|
|
452
|
+
|
|
453
|
+
Images are resizable out of the box (the official `@tiptap/extension-image` `resize` capability): hover an image — the handles appear on hover only — and drag a side handle. The size is committed as integer-pixel node attributes (`width` / `height`) — not inline styles — so it survives save and reload.
|
|
454
|
+
|
|
455
|
+
CommonMark has no size syntax, so **a resized image serializes to inline HTML**: `<img src="…" alt="…" width="440" height="330">`, while untouched images keep the standard `` form. The editor, the preview and the server renderer all parse that tag back through one shared tokenizer (it claims `<img …>` before the `window.DOMParser` path can run), so sizes round-trip in every pipeline — including SSR, where raw inline HTML would otherwise be escaped to literal text. Dragging writes `width`/`height`; export `resetImageSize(editor)` clears them and returns the image to the clean `` syntax (wire it to your own button — the library renders no UI for it).
|
|
456
|
+
|
|
457
|
+
```tsx
|
|
458
|
+
<MarkdownWysiwygEditor
|
|
459
|
+
imageResize={{ minWidth: 80, minHeight: 80 }} // default: true
|
|
460
|
+
/>
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Defaults: two side handles (`left` / `right`, matching the official demo, shown on hover only), aspect ratio always locked (`alwaysPreserveAspectRatio: true`), a **minimum of 80×80 px**, and a **maximum that follows the editor's content width** — images cannot be dragged past the layout; the bound is re-measured before every drag, so window resizes are picked up. Pin your own bounds with `minWidth` / `minHeight` / `maxWidth` / `maxHeight` (the library injects the max into the official `ResizableNodeView`, which has no max option of its own). Locking keeps the committed size consistent with how read-only rendering scales the image (CSS `height: auto`), so non-uniformly stretched images cannot "snap back" in previews; pass `alwaysPreserveAspectRatio: false` to allow free stretching anyway. Read-only editors never show handles, and `imageResize={false}` disables the feature entirely.
|
|
464
|
+
|
|
465
|
+
#### Image alignment
|
|
466
|
+
|
|
467
|
+
Hover an image and a small toolbar appears above it — left / center / right, the same hover-handle interaction as the table row/column handles. Alignment is stored as the `align` node attribute and rendered as **`data-align`** — the attribute name the official `image-align-button` uses — so existing `img[data-align="center"]` styles keep working. Keyboard shortcuts match the official component: <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>L</kbd> / <kbd>E</kbd> / <kbd>R</kbd> with an image selected (or the cursor right next to it).
|
|
468
|
+
|
|
469
|
+
Like size, alignment has no markdown syntax, so an aligned image serializes to `<img … data-align="center">` (untouched images stay ``) and the same tokenizer parses it back — editor, preview and server renderer all agree. For custom UIs the library exports `setImageAlign(editor, align)` / `isImageAlignActive(editor, align)` (selection-based) and `setImageAlignAt(editor, element, align)` (DOM-based); the hover toolbar's text comes from `labels.imageAlign*`.
|
|
470
|
+
|
|
471
|
+
#### Image caption
|
|
472
|
+
|
|
473
|
+
Hover an image, click the caption button in the hover toolbar and an input opens right there — <kbd>Enter</kbd> commits, <kbd>Esc</kbd> cancels, clicking away commits too. The caption renders under the image (small muted text, aligned with the image) and is stored as the `caption` node attribute.
|
|
474
|
+
|
|
475
|
+
In markdown a captioned image serializes to a `<figure>` block (standard HTML semantics — GitHub renders it the same way):
|
|
476
|
+
|
|
477
|
+
```html
|
|
478
|
+
<figure>
|
|
479
|
+
<img src="…" alt="…">
|
|
480
|
+
<figcaption>Figure 1: something</figcaption>
|
|
481
|
+
</figure>
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
Images without a caption keep the plain `` form. Editor, preview and server renderer share the same tokenizer, so captions round-trip everywhere — including SSR. Tooling for custom UIs: `setImageCaption(editor, text)` / `setImageCaptionAtPos(editor, pos, text)`; hover-toolbar labels via `labels.imageCaption` / `imageCaptionPlaceholder`. The official side has nothing for this (`@tiptap/extension-figure` does not exist on npm), so the whole path is native to this library.
|
|
485
|
+
|
|
409
486
|
### `<EditorToolbar>` (client)
|
|
410
487
|
|
|
411
|
-
The row
|
|
488
|
+
The row stays centred and **scrolls horizontally** when the controls no longer fit — a single line, never wrapped (`justify-content: safe center` keeps both ends reachable once it overflows). The full set (search entry included) needs about 1045 px of container width; narrower containers scroll. `onSearch` renders the magnifier just left of the More menu.
|
|
412
489
|
|
|
413
490
|
| Prop | Type | Description |
|
|
414
491
|
| --- | --- | --- |
|
|
415
492
|
| `editor` | `Editor` | The instance from `onEditorReady`. |
|
|
416
|
-
| `onImageUpload` | `(file: File) => Promise<string>` |
|
|
493
|
+
| `onImageUpload` | `(file: File) => Promise<string>` | **Legacy** direct-upload path: picking a file inserts the returned URL. Ignored when the editor registers the `imageUpload` extension (configure that on `<MarkdownWysiwygEditor>` instead); omit both and no image button renders. |
|
|
417
494
|
| `onError` | `(err, source?: 'image' \| 'markdown' \| 'import') => void` | Side-effect error callback (e.g. failed upload). |
|
|
418
495
|
| `onImportDocument` | `(file, ctx) => Promise<ImportDocumentResult \| string>` | Converts a non-Markdown file to Markdown. `ctx.signal` aborts on cancel/unmount; `ctx.onProgress` reports upload/convert progress back. Omit and Import only takes `.md`. |
|
|
419
496
|
| `importAccept` | `string` | Extra `accept` used by the default Document menu item when `importMenuItems` is omitted. |
|
|
@@ -427,6 +504,7 @@ The row is centred and, when the controls no longer fit the container, **wraps o
|
|
|
427
504
|
| `labels.mathPlaceholder` / `mathDone` / `mathNewInline` / `mathNewBlock` | `string` | Equation editor (empty chip/hint, input, Done). |
|
|
428
505
|
| `labels.tableRowMenu` / `tableColumnMenu` | `string` | Aria-labels of the table hover handles. |
|
|
429
506
|
| `labels.linkPrompt` / `linkApply` / `linkOpen` / `linkRemove` / `linkInvalid` | `string` | Link popover: field placeholder, apply, open in new window, remove, rejected-address hint. |
|
|
507
|
+
| `labels.imageAlign` / `imageAlignLeft` / `imageAlignCenter` / `imageAlignRight` | `string` | Hover alignment toolbar (rendered by the toolbar, appears over the hovered image): toolbar aria-label and the three button labels. |
|
|
430
508
|
|
|
431
509
|
#### Table editing (hover handles)
|
|
432
510
|
|
|
@@ -511,3 +589,7 @@ The commands are exported for headless or self-drawn UIs: `applyLink`, `removeLi
|
|
|
511
589
|
## License
|
|
512
590
|
|
|
513
591
|
MIT © CatsInJune
|
|
592
|
+
|
|
593
|
+
Third-party content used or adapted by this project — Lucide icons, Tiptap UI Components
|
|
594
|
+
(ported component logic and icons), KaTeX styles/fonts, and the npm dependency licenses —
|
|
595
|
+
is credited in [THIRD_PARTY_LICENSES.md](./THIRD_PARTY_LICENSES.md).
|
package/THIRD_PARTY_LICENSES.md
CHANGED
|
@@ -52,7 +52,48 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
52
52
|
SOFTWARE.
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
## 2.
|
|
55
|
+
## 2. Tiptap UI Components — MIT License
|
|
56
|
+
|
|
57
|
+
本库部分组件的交互与实现参照(移植自)[tiptap-ui-components](https://github.com/ueberdosis/tiptap-ui-components)
|
|
58
|
+
(MIT 许可证),按本库的架构、命名与视觉体系重写,并在各源码文件头部标注来源:
|
|
59
|
+
|
|
60
|
+
| 官方组件 | 本库落点 | 移植范围 | 时间 |
|
|
61
|
+
| --- | --- | --- | --- |
|
|
62
|
+
| `image-upload-node` | `src/imageUpload.ts`、`src/components/ImageUploadView.tsx` | 上传占位块的交互与组件结构(拖拽区 / 文件队列 / 进度 / 中止)、默认文案与图标 path(文档形状、折角、云上传) | 2026-10-10 |
|
|
63
|
+
| `useLinkPopover` | `src/linkEditing.ts` | 链接编辑逻辑(整条链接的范围扩展、空选区插地址文本、`preventAutolink` 元数据、打开前的协议白名单);浮层外壳自绘 | 2026-10-09 |
|
|
64
|
+
|
|
65
|
+
移植过程中修正了官方两处缺陷(`maxSize: 0` 被误判为超限、多文件时文件名与 URL 错位),
|
|
66
|
+
并为 Markdown 往返补了空序列化等适配——这些属于本库的改动,不改变上述归属。
|
|
67
|
+
`src/findReplace.ts` 的 IME 组合守卫等交互判据与之同款(该文件的主逻辑由同为 MIT 的
|
|
68
|
+
`@tiptap/extension-find-and-replace` 包承担,见第 3 节)。
|
|
69
|
+
|
|
70
|
+
### The MIT License
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
MIT License
|
|
74
|
+
|
|
75
|
+
Copyright (c) 2025 Tiptap GmbH
|
|
76
|
+
|
|
77
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
78
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
79
|
+
in the Software without restriction, including without limitation the rights
|
|
80
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
81
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
82
|
+
furnished to do so, subject to the following conditions:
|
|
83
|
+
|
|
84
|
+
The above copyright notice and this permission notice shall be included in all
|
|
85
|
+
copies or substantial portions of the Software.
|
|
86
|
+
|
|
87
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
88
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
89
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
90
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
91
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
92
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
93
|
+
SOFTWARE.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 3. 运行时依赖(npm dependencies)
|
|
56
97
|
|
|
57
98
|
以下依赖通过 npm 以 `dependencies` 声明引入,构建时均 external(不打入 `dist/`),各自许可证随 `node_modules` 自动分发。此处仅做汇总。
|
|
58
99
|
|
|
@@ -65,14 +106,17 @@ SOFTWARE.
|
|
|
65
106
|
| `@tiptap/extension-code-block-lowlight` | MIT |
|
|
66
107
|
| `@tiptap/extension-file-handler` | MIT |
|
|
67
108
|
| `@tiptap/extension-find-and-replace` | MIT |
|
|
109
|
+
| `@tiptap/extension-heading` | MIT |
|
|
68
110
|
| `@tiptap/extension-highlight` | MIT |
|
|
69
111
|
| `@tiptap/extension-image` | MIT |
|
|
112
|
+
| `@tiptap/extension-paragraph` | MIT |
|
|
70
113
|
| `@tiptap/extension-subscript` | MIT |
|
|
71
114
|
| `@tiptap/extension-superscript` | MIT |
|
|
72
115
|
| `@tiptap/extension-table` | MIT |
|
|
73
116
|
| `@tiptap/extension-table-of-contents` | MIT |
|
|
74
117
|
| `@tiptap/extension-task-item` | MIT |
|
|
75
118
|
| `@tiptap/extension-task-list` | MIT |
|
|
119
|
+
| `@tiptap/extension-text-align` | MIT |
|
|
76
120
|
| `@tiptap/extension-text-style` | MIT |
|
|
77
121
|
| `@tiptap/markdown` | MIT |
|
|
78
122
|
| `@tiptap/pm` | MIT |
|
|
@@ -423,13 +423,13 @@ ${fenceBody}
|
|
|
423
423
|
}
|
|
424
424
|
return out;
|
|
425
425
|
}
|
|
426
|
-
const editorHost = "
|
|
427
|
-
const findBarAnchor = "
|
|
428
|
-
const findBar = "
|
|
429
|
-
const editorScroll = "
|
|
430
|
-
const editorContent = "
|
|
431
|
-
const shortcutFab = "
|
|
432
|
-
const shortcutFabHosted = "
|
|
426
|
+
const editorHost = "_editorHost_yyqcs_6";
|
|
427
|
+
const findBarAnchor = "_findBarAnchor_yyqcs_13";
|
|
428
|
+
const findBar = "_findBar_yyqcs_13";
|
|
429
|
+
const editorScroll = "_editorScroll_yyqcs_5";
|
|
430
|
+
const editorContent = "_editorContent_yyqcs_30";
|
|
431
|
+
const shortcutFab = "_shortcutFab_yyqcs_570";
|
|
432
|
+
const shortcutFabHosted = "_shortcutFabHosted_yyqcs_598";
|
|
433
433
|
const styles = {
|
|
434
434
|
editorHost,
|
|
435
435
|
findBarAnchor,
|
|
@@ -459,4 +459,4 @@ export {
|
|
|
459
459
|
parseChartFenceBody as p,
|
|
460
460
|
serializeChartPayload as s
|
|
461
461
|
};
|
|
462
|
-
//# sourceMappingURL=ReportContent-
|
|
462
|
+
//# sourceMappingURL=ReportContent-B8jIzhy3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReportContent-B8jIzhy3.js","sources":["../src/citationUtils.ts","../src/chart/columnMatching.ts","../src/chart/types.ts","../src/chart/parse.ts","../src/ReportContent.tsx"],"sourcesContent":["import type { JSONContent } from '@tiptap/core';\n\nexport interface SourceRef {\n /** 与 markdown `[^n]` 中的 n 对齐(字符串,如 `\"1\"`)。 */\n index: string;\n url?: string;\n title?: string;\n /** 原文片段等;库本身不渲染 Popover,留给消费方。 */\n excerpt?: string;\n}\n\nfunction escapeAttr(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/\"/g, '"')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n}\n\n/**\n * 将 markdown 中能匹配到 `sources` 的 `[^n]` 换成带 data-* 的\n * `<span class=\"citation-ref\">`(**不是** `<sup>`,避免被 Superscript 吃掉、\n * 也避免视觉上标)。未匹配的 `[^n]` 原样保留,留给 CitationRef tokenizer。\n *\n * 浏览器端 @tiptap/markdown 可解析内联 HTML;server 端无 DOMParser 时请改用\n * {@link applyCitationSources}(先 parse 再补 attrs)。\n */\nexport function enrichMarkdownCitations(\n markdown: string,\n sources: SourceRef[],\n): string {\n if (!sources.length) return markdown;\n\n return markdown.replace(/\\[\\^(\\d+)\\]/g, (match, num: string) => {\n const source = sources.find((s) => s.index === num);\n if (!source) return match;\n\n const attrs = [`class=\"citation-ref\"`, `data-index=\"${num}\"`];\n if (source.url) attrs.push(`data-url=\"${escapeAttr(source.url)}\"`);\n if (source.title) attrs.push(`data-title=\"${escapeAttr(source.title)}\"`);\n\n if (source.url) {\n return `<span ${attrs.join(' ')}><a href=\"${escapeAttr(source.url)}\" title=\"${escapeAttr(source.title || '')}\" target=\"_blank\" rel=\"noopener\">${num}</a></span>`;\n }\n return `<span ${attrs.join(' ')}>${num}</span>`;\n });\n}\n\nfunction patchCitationNode(\n node: JSONContent,\n byIndex: Map<string, SourceRef>,\n): JSONContent {\n if (node.type === 'citationRef') {\n const index = String(node.attrs?.index ?? '');\n const source = byIndex.get(index);\n if (!source) return node;\n return {\n ...node,\n attrs: {\n ...node.attrs,\n index,\n url: source.url ?? node.attrs?.url ?? null,\n title: source.title ?? node.attrs?.title ?? null,\n },\n };\n }\n\n if (!node.content?.length) return node;\n return {\n ...node,\n content: node.content.map((child) => patchCitationNode(child, byIndex)),\n };\n}\n\n/**\n * 在已 parse 的 Tiptap JSON 上,按 `sources` 给 `citationRef` 节点补 url/title。\n * SSR / 无 DOMParser 环境的首选路径。\n */\nexport function applyCitationSources(\n doc: JSONContent,\n sources: SourceRef[],\n): JSONContent {\n if (!sources.length) return doc;\n const byIndex = new Map(sources.map((s) => [s.index, s]));\n return patchCitationNode(doc, byIndex);\n}\n\n/** 单条脚注定义行:行首 0–3 空格 + `[^数字]:`。 */\nconst FOOTNOTE_DEF_RE = /^ {0,3}\\[\\^(\\d+)\\]:[ \\t]?(.*)$/;\n\n/** 内联链接:`[文本](url)`。URL 支持 `<...>` 包裹(可含空格)或一层配对括号(维基百科式)。 */\nconst MD_LINK_RE =\n /\\[([^\\]]*)\\]\\(\\s*(?:<([^<>]*)>|((?:[^()\\s]|\\([^()]*\\))+)(?:[ \\t]+\"[^\"]*\")?)\\s*\\)/;\nconst MD_LINK_RE_G = new RegExp(MD_LINK_RE.source, 'g');\n\n/** 把定义内容压成纯文本(链接只留文本),供 excerpt 与 title 兜底。 */\nfunction flattenFootnoteContent(content: string): string {\n return content\n .replace(\n MD_LINK_RE_G,\n (_match, text: string, angleUrl: string, plainUrl: string) =>\n text || angleUrl || plainUrl || '',\n )\n .trim();\n}\n\nexport interface ExtractedFootnotes {\n /** 剥离定义行后的正文(首尾与多余空行已收敛)。 */\n markdown: string;\n /** 解析出的来源列表,同一 label 重复定义取首次。 */\n sources: SourceRef[];\n}\n\n/**\n * 把 markdown 中的 GFM 脚注定义行(`[^n]: …`)抽成 {@link SourceRef} 并从正文剥离。\n *\n * 与 {@link enrichMarkdownCitations} / {@link applyCitationSources} 同属脚注一族:\n * 它们消费 `sources` 处理 `[^n]` 标记这一半,本函数生产 `sources`、处理定义行\n * 另一半——不剥离,定义行会以普通段落渲染;不抽取,标记没有 url/title 可补。\n * 典型用法(纯字符串变换,流式每帧重算也便宜):\n *\n * ```ts\n * const { markdown: body, sources } = extractFootnoteSources(raw);\n * // <MarkdownPreview markdown={body} sources={sources} … />\n * ```\n *\n * 规则与边界:\n * - label 仅支持数字,与 enrich / CitationRef tokenizer 的 `\\[\\^(\\d+)\\]` 一致;\n * 非数字定义行不识别、原样留在正文;\n * - 围栏代码块(``` / ~~~,判定与 {@link stabilizeMarkdown} 一致)内的定义行跳过;\n * 缩进代码块不识别(已知限制);\n * - 续行:紧跟定义的缩进(≥4 空格或 Tab)非空行并入内容,空行结束定义;\n * - 内容中第一个内联链接取作 url、链接文本作 title;excerpt 取纯文本全文;\n * 无链接时 title 取纯文本首行;\n * - 流式半行(如 `[^1]: [原文](htt`)按 best-effort 解析,下一帧自愈。\n */\nexport function extractFootnoteSources(markdown: string): ExtractedFootnotes {\n if (!markdown || !markdown.includes('[^')) {\n return { markdown, sources: [] };\n }\n\n const lines = markdown.split('\\n');\n const bodyLines: string[] = [];\n const sources: SourceRef[] = [];\n const seen = new Set<string>();\n let removed = false;\n\n let inFence = false;\n let fenceChar = '`';\n let fenceLen = 3;\n\n for (let i = 0; i < lines.length; i += 1) {\n const line = lines[i];\n\n const fence = line.match(/^(`{3,}|~{3,})/);\n if (fence) {\n const char = fence[1][0];\n if (!inFence) {\n inFence = true;\n fenceChar = char;\n fenceLen = fence[1].length;\n } else if (\n char === fenceChar &&\n fence[1].length >= fenceLen &&\n /^[`~]+\\s*$/.test(line)\n ) {\n inFence = false;\n }\n bodyLines.push(line);\n continue;\n }\n if (inFence) {\n bodyLines.push(line);\n continue;\n }\n\n const def = FOOTNOTE_DEF_RE.exec(line);\n if (!def) {\n bodyLines.push(line);\n continue;\n }\n removed = true;\n\n const parts = [def[2]];\n let j = i + 1;\n while (\n j < lines.length &&\n lines[j].trim() &&\n /^(?: {4,}|\\t)/.test(lines[j])\n ) {\n parts.push(lines[j].trim());\n j += 1;\n }\n i = j - 1;\n\n const index = def[1];\n if (seen.has(index)) continue;\n seen.add(index);\n\n const content = parts.join('\\n');\n const link = MD_LINK_RE.exec(content);\n const source: SourceRef = { index };\n if (link) {\n const url = link[2] ?? link[3];\n if (url) source.url = url;\n const text = link[1]?.trim();\n if (text) source.title = text;\n }\n const plain = flattenFootnoteContent(content);\n if (plain) {\n source.excerpt = plain;\n if (!source.title) source.title = plain.split('\\n')[0];\n }\n sources.push(source);\n }\n\n if (!removed) {\n return { markdown, sources: [] };\n }\n\n return {\n markdown: bodyLines\n .join('\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .replace(/^\\n+/, '')\n .replace(/\\s+$/, ''),\n sources,\n };\n}\n","/**\n * Loose header ↔ config field matching for chart data tables.\n * Zero runtime deps beyond string/regex.\n */\n\nconst TRAILING_UNIT_SUFFIX_PATTERN = /^(\\s*[((][^))]+[))])+\\s*$/;\n\n/** Exact match, or column = field + parenthetical unit suffix. */\nexport function columnKeyMatchesConfiguredField(\n columnKey: string,\n configuredField: string,\n): boolean {\n const ck = (columnKey || '').trim();\n const f = (configuredField || '').trim();\n if (!ck || !f) return false;\n if (ck === f) return true;\n if (!ck.startsWith(f)) return false;\n const rest = ck.slice(f.length);\n return rest === '' || TRAILING_UNIT_SUFFIX_PATTERN.test(rest);\n}\n\n/**\n * Resolve configured x/y name to a real column dataIndex.\n * Returns original field when no match (caller decides degrade).\n */\nexport function resolveChartAxisFieldToColumnKey(\n configuredField: string | undefined,\n columnKeys: string[],\n): string | undefined {\n if (configuredField === undefined || configuredField === null) {\n return configuredField;\n }\n const f = configuredField.trim();\n if (!f) return configuredField;\n const keySet = new Set(columnKeys);\n if (keySet.has(f)) return f;\n const hit = columnKeys.find((k) => columnKeyMatchesConfiguredField(k, f));\n return hit ?? configuredField;\n}\n\nexport function normalizeChartConfigAxisFields<\n T extends { x?: string; y?: string; groupBy?: string; filterBy?: string; colorLegend?: string; sortBy?: string },\n>(cfg: T, columnKeys: string[]): T {\n return {\n ...cfg,\n ...(cfg.x !== undefined\n ? { x: resolveChartAxisFieldToColumnKey(cfg.x, columnKeys) }\n : {}),\n ...(cfg.y !== undefined\n ? { y: resolveChartAxisFieldToColumnKey(cfg.y, columnKeys) }\n : {}),\n ...(cfg.groupBy !== undefined\n ? { groupBy: resolveChartAxisFieldToColumnKey(cfg.groupBy, columnKeys) }\n : {}),\n ...(cfg.filterBy !== undefined\n ? {\n filterBy: resolveChartAxisFieldToColumnKey(cfg.filterBy, columnKeys),\n }\n : {}),\n ...(cfg.colorLegend !== undefined\n ? {\n colorLegend: resolveChartAxisFieldToColumnKey(\n cfg.colorLegend,\n columnKeys,\n ),\n }\n : {}),\n ...(cfg.sortBy !== undefined\n ? { sortBy: resolveChartAxisFieldToColumnKey(cfg.sortBy, columnKeys) }\n : {}),\n };\n}\n","/** MVP chart types (subset). */\nexport const MVP_CHART_TYPES = [\n 'line',\n 'bar',\n 'column',\n 'pie',\n 'donut',\n 'area',\n] as const;\n\nexport type MvpChartType = (typeof MVP_CHART_TYPES)[number];\n\nexport function isMvpChartType(value: unknown): value is MvpChartType {\n return (\n typeof value === 'string' &&\n (MVP_CHART_TYPES as readonly string[]).includes(value)\n );\n}\n\n/**\n * Flat chart config (author / LLM form).\n * `subgraphBy` is accepted but ignored in v1.\n */\nexport interface ChartConfig {\n chartType: string;\n x?: string;\n y?: string;\n title?: string;\n height?: number;\n dataTime?: string;\n groupBy?: string;\n filterBy?: string;\n colorLegend?: string;\n sortBy?: string;\n subgraphBy?: string;\n [key: string]: unknown;\n}\n\nexport interface ChartColumn {\n title: string;\n dataIndex: string;\n}\n\n/** Normalized payload stored on the TipTap chart node. */\nexport interface ChartPayload {\n config: ChartConfig[];\n columns: ChartColumn[];\n dataSource: Record<string, unknown>[];\n}\n\n/** Optional host override for Chart.js colors / fonts. */\nexport interface ChartTheme {\n backgroundColor?: string;\n textColor?: string;\n gridColor?: string;\n borderColor?: string;\n fontFamily?: string;\n palette?: string[];\n}\n","import { normalizeChartConfigAxisFields } from './columnMatching';\nimport {\n isMvpChartType,\n type ChartColumn,\n type ChartConfig,\n type ChartPayload,\n type MvpChartType,\n} from './types';\n\nconst CHART_COMMENT_RE = /^<!--\\s*(\\[[\\s\\S]*?\\]|\\{[\\s\\S]*?\\})\\s*-->\\s*$/;\n\n/** GFM table: header + separator + optional body rows. */\nconst GFM_TABLE_BLOCK_RE =\n /^\\|(.+)\\|\\s*\\n\\|([-:\\s|]+)\\|\\s*\\n((?:\\|.*\\|\\s*\\n?)*)/;\n\nfunction splitRow(line: string): string[] {\n const trimmed = line.trim();\n const inner =\n trimmed.startsWith('|') && trimmed.endsWith('|')\n ? trimmed.slice(1, -1)\n : trimmed;\n return inner.split('|').map((c) => c.trim());\n}\n\nfunction isSeparatorRow(line: string): boolean {\n const cells = splitRow(line);\n return cells.length > 0 && cells.every((c) => /^:?-+:?$/.test(c));\n}\n\nfunction coerceCell(raw: string): string | number {\n const t = raw.trim();\n if (t === '') return t;\n const normalized = t.replace(/,/g, '');\n if (/^[-+]?(\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?$/.test(normalized)) {\n const n = Number(normalized);\n if (!Number.isNaN(n)) return n;\n }\n return t;\n}\n\n/** Parse a GFM markdown table into columns + rows. */\nexport function parseGfmTable(markdown: string): {\n columns: ChartColumn[];\n dataSource: Record<string, unknown>[];\n rawLength: number;\n} | null {\n const match = GFM_TABLE_BLOCK_RE.exec(markdown);\n if (!match) return null;\n const headerLine = `|${match[1]}|`;\n const sepLine = `|${match[2]}|`;\n if (!isSeparatorRow(sepLine)) return null;\n\n const headers = splitRow(headerLine);\n if (headers.length === 0 || headers.every((h) => !h)) return null;\n\n const columns: ChartColumn[] = headers.map((h) => ({\n title: h,\n dataIndex: h,\n }));\n\n const body = match[3] ?? '';\n const dataSource: Record<string, unknown>[] = [];\n for (const line of body.split('\\n')) {\n if (!line.trim()) continue;\n if (!line.includes('|')) continue;\n const cells = splitRow(line);\n if (cells.length === 0) continue;\n const row: Record<string, unknown> = {};\n headers.forEach((h, i) => {\n row[h] = coerceCell(cells[i] ?? '');\n });\n dataSource.push(row);\n }\n\n return {\n columns,\n dataSource,\n rawLength: match[0].length,\n };\n}\n\nfunction looksLikeChartCommentJson(parsed: unknown): boolean {\n if (Array.isArray(parsed)) {\n return parsed.some(\n (c) =>\n c &&\n typeof c === 'object' &&\n 'chartType' in (c as object) &&\n typeof (c as ChartConfig).chartType === 'string',\n );\n }\n if (parsed && typeof parsed === 'object') {\n const o = parsed as Record<string, unknown>;\n if (Array.isArray(o.config)) return true;\n if (typeof o.chartType === 'string') return true;\n }\n return false;\n}\n\n/** Unwrap comment JSON into a flat config list (may include non-MVP types). */\nexport function unwrapChartConfigs(raw: unknown): ChartConfig[] | null {\n if (raw == null) return null;\n if (Array.isArray(raw)) {\n return raw.filter(\n (c): c is ChartConfig =>\n !!c && typeof c === 'object' && typeof (c as ChartConfig).chartType === 'string',\n );\n }\n if (typeof raw !== 'object') return null;\n const o = raw as Record<string, unknown>;\n if (Array.isArray(o.config)) {\n return unwrapChartConfigs(o.config);\n }\n if (typeof o.chartType === 'string') {\n return [o as ChartConfig];\n }\n return null;\n}\n\nfunction inferPieAxes(\n cfg: ChartConfig,\n columns: ChartColumn[],\n): ChartConfig {\n if (cfg.x && cfg.y) return cfg;\n if (columns.length < 2) return cfg;\n return {\n ...cfg,\n x: cfg.x ?? columns[0].dataIndex,\n y: cfg.y ?? columns[1].dataIndex,\n };\n}\n\nfunction axisExists(field: string | undefined, keys: string[]): boolean {\n if (!field) return false;\n return keys.includes(field);\n}\n\n/**\n * Keep only MVP chart configs that have resolvable axes.\n * Returns null when nothing left (caller should leave as plain table).\n */\nexport function normalizeChartPayload(\n configs: ChartConfig[],\n columns: ChartColumn[],\n dataSource: Record<string, unknown>[],\n): ChartPayload | null {\n const keys = columns.map((c) => c.dataIndex);\n const normalized: ChartConfig[] = [];\n\n for (const raw of configs) {\n if (!isMvpChartType(raw.chartType)) continue;\n let cfg = normalizeChartConfigAxisFields({ ...raw }, keys);\n const type = cfg.chartType as MvpChartType;\n if (type === 'pie' || type === 'donut') {\n cfg = inferPieAxes(cfg, columns);\n }\n if (!axisExists(cfg.x, keys) || !axisExists(cfg.y, keys)) continue;\n // v1: ignore subgraphBy\n if (cfg.subgraphBy) {\n const { subgraphBy: _drop, ...rest } = cfg;\n cfg = rest;\n }\n normalized.push(cfg);\n }\n\n if (normalized.length === 0) return null;\n return { config: normalized, columns, dataSource };\n}\n\n/** Parse ```chart / ```json-chart / ```tmr-chart fence body JSON. */\nexport function parseChartFenceBody(body: string): ChartPayload | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(body.trim());\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== 'object') return null;\n const o = parsed as Record<string, unknown>;\n\n // Full form\n if (Array.isArray(o.columns) || Array.isArray(o.dataSource)) {\n const configs = unwrapChartConfigs(o.config ?? o) ?? [];\n const columns = (o.columns as ChartColumn[]) ?? [];\n const dataSource =\n (o.dataSource as Record<string, unknown>[]) ??\n (o.data as Record<string, unknown>[]) ??\n [];\n if (!columns.length) {\n // Simple { chartType, x, y, data }\n if (Array.isArray(o.data) && typeof o.chartType === 'string') {\n const simpleCols: ChartColumn[] = [\n { title: String(o.x ?? 'x'), dataIndex: 'x' },\n { title: String(o.y ?? 'y'), dataIndex: 'y' },\n ];\n const rows = (o.data as unknown[]).map((item) => {\n if (item && typeof item === 'object') return item as Record<string, unknown>;\n return {};\n });\n return normalizeChartPayload(\n [{ chartType: String(o.chartType), x: 'x', y: 'y', title: o.title as string | undefined }],\n simpleCols,\n rows,\n );\n }\n return null;\n }\n const colObjs: ChartColumn[] = columns.map((c) =>\n typeof c === 'string'\n ? { title: c, dataIndex: c }\n : {\n title: String((c as ChartColumn).title ?? (c as ChartColumn).dataIndex),\n dataIndex: String((c as ChartColumn).dataIndex ?? (c as ChartColumn).title),\n },\n );\n return normalizeChartPayload(configs, colObjs, dataSource);\n }\n\n const configs = unwrapChartConfigs(parsed);\n if (!configs) return null;\n return null;\n}\n\n/**\n * Try to parse leading `<!-- chart json -->` + GFM table from `src`.\n * Returns payload + `raw`(`src` 里被消费掉的**精确前缀**,调用方按 `raw.length`\n * 跳过这一段), or null.\n */\nexport function tryParseCommentTableChart(\n src: string,\n): { payload: ChartPayload; raw: string } | null {\n const trimmedStart = src.replace(/^\\s*/, '');\n const offset = src.length - trimmedStart.length;\n if (!trimmedStart.startsWith('<!--')) return null;\n\n const endComment = trimmedStart.indexOf('-->');\n if (endComment < 0) return null;\n const commentBlock = trimmedStart.slice(0, endComment + 3);\n const cm = CHART_COMMENT_RE.exec(commentBlock.trim());\n if (!cm) return null;\n\n let json: unknown;\n try {\n json = JSON.parse(cm[1]);\n } catch {\n return null;\n }\n if (!looksLikeChartCommentJson(json)) return null;\n\n const configs = unwrapChartConfigs(json);\n if (!configs || configs.length === 0) return null;\n // All table-only → not a chart\n if (configs.every((c) => c.chartType === 'table')) return null;\n\n const afterCommentRaw = trimmedStart.slice(endComment + 3);\n // 注释与表格之间隔空行时折成一个 \\n 再解析(表格内容本身不受影响)。\n const afterComment = afterCommentRaw.replace(/^\\s*\\n/, '\\n');\n const tableStart = afterComment.search(/\\|/);\n if (tableStart < 0) return null;\n const beforeTable = afterComment.slice(0, tableStart);\n if (beforeTable.replace(/\\s/g, '') !== '') return null;\n\n const tableMd = afterComment.slice(tableStart);\n const table = parseGfmTable(tableMd);\n if (!table) return null;\n\n const payload = normalizeChartPayload(\n configs,\n table.columns,\n table.dataSource,\n );\n if (!payload) return null;\n\n // 表格起点要在**原文**里定位:折过的 afterComment 比原文短,拿它当基准会让\n // raw 短掉那几个空行字符,调用方按 raw.length 切 src 就会多吃掉后一个字符\n // —— 表格是最后一块时,被吃掉的正是表格收尾的 `|`。\n const tableStartInSrc = afterCommentRaw.search(/\\|/);\n const raw = src.slice(\n 0,\n offset + commentBlock.length + tableStartInSrc + table.rawLength,\n );\n\n return { payload, raw };\n}\n\n/** Serialize payload to flat comment + GFM table (author form). */\nexport function serializeChartPayload(payload: ChartPayload): string {\n const flat =\n payload.config.length === 1 ? payload.config[0] : payload.config;\n const comment = `<!-- ${JSON.stringify(flat)} -->`;\n const headers = payload.columns.map((c) => c.title);\n const keys = payload.columns.map((c) => c.dataIndex);\n const header = `| ${headers.join(' | ')} |`;\n const sep = `| ${headers.map(() => '---').join(' | ')} |`;\n const rows = payload.dataSource.map((row) => {\n const cells = keys.map((k) => {\n const v = row[k];\n if (v == null) return '';\n return String(v);\n });\n return `| ${cells.join(' | ')} |`;\n });\n return [comment, '', header, sep, ...rows].join('\\n');\n}\n\n/**\n * Replace valid comment+table chart pairs with ```tmr-chart fences\n * so TipTap markdown can tokenize a single fence.\n */\nexport function normalizeChartMarkdown(markdown: string): string {\n if (!markdown) return markdown;\n let rest = markdown;\n let out = '';\n\n while (rest.length > 0) {\n const idx = rest.indexOf('<!--');\n if (idx < 0) {\n out += rest;\n break;\n }\n out += rest.slice(0, idx);\n const attempt = tryParseCommentTableChart(rest.slice(idx));\n if (!attempt) {\n // Skip this comment opener without consuming as chart\n out += '<!--';\n rest = rest.slice(idx + 4);\n continue;\n }\n const fenceBody = JSON.stringify({\n config: attempt.payload.config,\n columns: attempt.payload.columns,\n dataSource: attempt.payload.dataSource,\n });\n out += `\\`\\`\\`tmr-chart\\n${fenceBody}\\n\\`\\`\\``;\n rest = rest.slice(idx + attempt.raw.length);\n // Preserve a newline after fence if original had trailing content\n if (rest.startsWith('\\n')) {\n /* keep */\n } else if (rest.length > 0) {\n out += '\\n';\n }\n }\n\n return out;\n}\n","import type { HTMLAttributes } from 'react';\nimport styles from './styles/content.module.css';\n\nexport interface ReportContentProps extends HTMLAttributes<HTMLDivElement> {\n /** `renderReportHtml(...).html` 产出的正文 HTML 字符串。 */\n html: string;\n}\n\n/**\n * 只读静态正文渲染(RSC / SSR 安全,无 client 代码、无编辑器实例)。\n *\n * 把 `renderReportHtml` 产出的 HTML 注入容器,并套上与编辑器 / 只读预览**同一个\n * 规范正文类**,保证「编辑所见 = 预览所见 = 发布后读者所见」。\n * 用于阅读页首屏直出(SEO 可见),无需 JS 即可显示。\n */\nexport function ReportContent({ html, className, ...rest }: ReportContentProps) {\n return (\n <div\n {...rest}\n className={\n className ? `${styles.editorContent} ${className}` : styles.editorContent\n }\n dangerouslySetInnerHTML={{ __html: html }}\n />\n );\n}\n"],"names":["configs"],"mappings":"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"}
|
package/dist/chart/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** MVP chart types
|
|
1
|
+
/** MVP chart types (subset). */
|
|
2
2
|
export declare const MVP_CHART_TYPES: readonly ["line", "bar", "column", "pie", "donut", "area"];
|
|
3
3
|
export type MvpChartType = (typeof MVP_CHART_TYPES)[number];
|
|
4
4
|
export declare function isMvpChartType(value: unknown): value is MvpChartType;
|
|
@@ -29,7 +29,7 @@ export interface CommentAnchorResult {
|
|
|
29
29
|
ranges: CommentRange[];
|
|
30
30
|
status: CommentAnchorStatus;
|
|
31
31
|
}
|
|
32
|
-
/** djb2
|
|
32
|
+
/** djb2 哈希(捕获端与消费端必须用同一个实现;接入时对齐)。 */
|
|
33
33
|
export declare function blockTextHash(text: string): string;
|
|
34
34
|
/** 主入口:评论列表 → 每条的 doc 区间 + 状态。 */
|
|
35
35
|
export declare function mapCommentAnchors(doc: Node, comments: CommentAnchorInput[]): CommentAnchorResult[];
|
|
@@ -78,7 +78,7 @@ export interface EditorToolbarProps {
|
|
|
78
78
|
}
|
|
79
79
|
export type { ImportMenuItem };
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
81
|
+
* 顶部操作栏(零 UI 框架依赖,Radix Popover 菜单 + 内联 SVG 图标)。位置由父级控制,
|
|
82
82
|
* 这里只渲染按钮 + 反映 editor 激活态。不含业务耦合,扩展项经 extraToolbarItems 注入。
|
|
83
83
|
*/
|
|
84
84
|
export declare function EditorToolbar({ editor, onImageUpload, onError, onImportDocument, importAccept, importMenuItems, showImport, labels, onSearch, searchActive, extraToolbarItems, className, }: EditorToolbarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Editor } from '@tiptap/core';
|
|
2
|
+
import { ToolbarLabels } from '../labels';
|
|
3
|
+
export interface ImageAlignToolsProps {
|
|
4
|
+
/** 编辑器实例。只读态不出工具条。 */
|
|
5
|
+
editor: Editor;
|
|
6
|
+
/** 已合并默认值的工具栏文案(工具条与三个按钮的 aria-label 都取自它)。 */
|
|
7
|
+
labels: ToolbarLabels;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 图片对齐工具条(悬停式,与表格悬停手柄同一套交互):鼠标落在图片上时,图片上方
|
|
11
|
+
* (放不下就下方)浮出一排左 / 中 / 右按钮,点一下即对齐,当前对齐方向高亮。
|
|
12
|
+
*
|
|
13
|
+
* fixed 定位 + portal 到 body:它要贴着图片边缘,而工具栏并不在编辑器 DOM 内。
|
|
14
|
+
* 滚动 / 缩放时重新量悬停的那张图让工具条跟着走。
|
|
15
|
+
*
|
|
16
|
+
* 点击走 {@link setImageAlignAt}(按 DOM 位置改 attrs):鼠标只是飘过图片时图片
|
|
17
|
+
* 并未被选中,不能像快捷键那样走 selection。
|
|
18
|
+
*/
|
|
19
|
+
export declare function ImageAlignTools({ editor, labels }: ImageAlignToolsProps): import('react').ReactPortal | null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NodeViewProps } from '@tiptap/react';
|
|
2
|
+
/**
|
|
3
|
+
* 图片上传占位块的 NodeView:空态是点击 / 拖拽区,排队后是文件行(进度条 + 百分比 +
|
|
4
|
+
* 移除),全部成功时就地把自己换成图片节点。上传函数与回调都来自扩展 options。
|
|
5
|
+
*
|
|
6
|
+
* 组件结构 / 文件队列 / 拖拽 / 中止逻辑按官方 UI Components 的 `image-upload-node`
|
|
7
|
+
* 移植(MIT,见 THIRD_PARTY_LICENSES.md 第 2 节),按本库的样式体系(CSS Modules +
|
|
8
|
+
* --tmr-* 变量)与 labels 注入重写。
|
|
9
|
+
*/
|
|
10
|
+
export declare function ImageUploadView(props: NodeViewProps): import("react").JSX.Element;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { AnyExtension, Editor } from '@tiptap/react';
|
|
2
|
+
import { ResizableNodeViewDirection } from '@tiptap/core';
|
|
2
3
|
import { SourceRef } from '../citationUtils';
|
|
3
4
|
import { RenderCitation } from '../citationTypes';
|
|
4
5
|
import { CommentClickPayload, CommentRef } from '../commentAnchor/commentTypes';
|
|
6
|
+
import { ImageUploadConfig } from '../imageUpload';
|
|
5
7
|
import { CodeBlockLabels, FindLabels, ShortcutLabels, SlashMenuLabels } from '../labels';
|
|
6
8
|
import { PendingAnchor } from '../pendingAnchor';
|
|
7
9
|
import { SelectionKind } from '../selectionKind';
|
|
8
10
|
import { TocItem } from '../toc/extractToc';
|
|
11
|
+
export declare const ImageWithConfirmDelete: import('@tiptap/core').Node<import('@tiptap/extension-image').ImageOptions, any>;
|
|
9
12
|
/** 浮动条相对其定位上下文的偏移(数字按 px)。内部路径的上下文是编辑器,portal 路径是宿主容器。 */
|
|
10
13
|
export interface FindBarOffset {
|
|
11
14
|
top?: number | string;
|
|
@@ -38,6 +41,32 @@ export interface MarkdownWysiwygEditorHandle {
|
|
|
38
41
|
*/
|
|
39
42
|
toggleFind: () => void;
|
|
40
43
|
}
|
|
44
|
+
/** 图片缩放的透传选项(官方 `@tiptap/extension-image` 的 resize 配置)。 */
|
|
45
|
+
export interface ImageResizeOptions {
|
|
46
|
+
/**
|
|
47
|
+
* 手柄方向,默认左右两条竖条(对齐官方 image-upload-node demo 的形态)。
|
|
48
|
+
* 例:`['top-right', 'bottom-right']` 只留右侧两角;八个方向全给则四角 + 四边。
|
|
49
|
+
*/
|
|
50
|
+
directions?: ResizableNodeViewDirection[];
|
|
51
|
+
/** 最小宽(px),默认 80。 */
|
|
52
|
+
minWidth?: number;
|
|
53
|
+
/** 最小高(px),默认 80。 */
|
|
54
|
+
minHeight?: number;
|
|
55
|
+
/**
|
|
56
|
+
* 最大宽(px)。**默认跟随编辑器内容区宽度**(拖不出布局,窗口变化时在下次拖拽前刷新
|
|
57
|
+
* 上限);显式传值则以此为准。官方 `Image` 的 resize 没有 max 入口,由本库注入
|
|
58
|
+
* `ResizableNodeView.maxSize`(该字段官方是公开的,拖拽约束每次都会读)。
|
|
59
|
+
*/
|
|
60
|
+
maxWidth?: number;
|
|
61
|
+
/** 最大高(px)。默认不单独限制(等比缩放时高度随宽度走)。 */
|
|
62
|
+
maxHeight?: number;
|
|
63
|
+
/**
|
|
64
|
+
* 是否始终锁定宽高比,默认 true(本库默认)。等比下"编辑态精确尺寸"与只读渲染的
|
|
65
|
+
* 按比例自适应(CSS `height: auto`)数学等价;显式关掉后,非等比拖出的图在只读
|
|
66
|
+
* 渲染里会按原图比例回弹——markdown 里存的仍是精确值。
|
|
67
|
+
*/
|
|
68
|
+
alwaysPreserveAspectRatio?: boolean;
|
|
69
|
+
}
|
|
41
70
|
export interface MarkdownWysiwygEditorProps {
|
|
42
71
|
/** 初始 markdown 内容。 */
|
|
43
72
|
initialMarkdown?: string;
|
|
@@ -129,7 +158,7 @@ export interface MarkdownWysiwygEditorProps {
|
|
|
129
158
|
*/
|
|
130
159
|
findBar?: boolean;
|
|
131
160
|
/**
|
|
132
|
-
* 把自带的浮动条挂到宿主的容器里(
|
|
161
|
+
* 把自带的浮动条挂到宿主的容器里(popup container 模式):给元素或返回元素的函数
|
|
133
162
|
* 即可,开合状态与 Cmd/Ctrl+F 仍归库——只换挂载点。适合条子被 `overflow: hidden` 祖先裁掉、
|
|
134
163
|
* 或想让它落在自己的头部 / 侧栏里。挂进去后**定位由宿主负责**(库不再加绝对定位),
|
|
135
164
|
* 且容器若在主题子树之外,记得把 `--tmr-*` 变量也带到那里。
|
|
@@ -180,6 +209,23 @@ export interface MarkdownWysiwygEditorProps {
|
|
|
180
209
|
slashMenu?: boolean;
|
|
181
210
|
/** 斜杠菜单的本地化文案。 */
|
|
182
211
|
slashMenuLabels?: Partial<SlashMenuLabels>;
|
|
212
|
+
/**
|
|
213
|
+
* 图片上传(对齐官方 ImageUploadNode 的交互):配置后工具栏的图片按钮变成
|
|
214
|
+
* 「插入上传块」——文档里出现拖拽 / 点击的占位块,上传进度就地显示,全部成功后就地
|
|
215
|
+
* 替换为图片。`upload` 是唯一必填项(网络 I/O 归宿主),签名 `(file, onProgress,
|
|
216
|
+
* signal) => Promise<url>`;收 `signal` 后宿主应在移除 / 清空 / 占位块被删时中断请求。
|
|
217
|
+
*
|
|
218
|
+
* 挂在这里而不是工具栏的 `onImageUpload`:按钮的行为由「编辑器里有没有注册
|
|
219
|
+
* imageUpload 扩展」决定,两处都传会以本配置为准。未配置时工具栏走旧的直传路径
|
|
220
|
+
* (点按钮直接开文件框 → 上传 → 插图片)。仅初始化时生效。
|
|
221
|
+
*/
|
|
222
|
+
imageUpload?: ImageUploadConfig;
|
|
223
|
+
/**
|
|
224
|
+
* 图片缩放(默认开):拖手柄改尺寸,松手把整数像素写进节点 attrs;markdown 里带尺寸的
|
|
225
|
+
* 图以 `<img src alt width height>` 保真(没拖过的图保持标准 ``,见
|
|
226
|
+
* imageMarkdown.ts)。传 `false` 关闭;传对象透传官方 resize 配置。仅初始化时生效。
|
|
227
|
+
*/
|
|
228
|
+
imageResize?: boolean | ImageResizeOptions;
|
|
183
229
|
}
|
|
184
230
|
/**
|
|
185
231
|
* 所见即所得 Markdown 编辑器(基于 Tiptap v3 + 官方 @tiptap/markdown)。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/extensions.d.ts
CHANGED
|
@@ -38,9 +38,21 @@ export declare const pureCodeBlock: import('@tiptap/core').Node<import('@tiptap/
|
|
|
38
38
|
* 纯版块级图片(无删除快捷键):server / 预览用。
|
|
39
39
|
* inline:false(官方默认)——研报里图片均为大图独占一行,块级图片可直接作为 doc
|
|
40
40
|
* 顶层子节点,避免「inline image 裸挂 doc 顶层」被 generateTocIds 严格校验判非法。
|
|
41
|
+
*
|
|
42
|
+
* markdown 往返接 imageMarkdown:带尺寸 / 对齐 / 描述(图注)的图序列化为
|
|
43
|
+
* `<img …>` / `<figure>…</figure>`、解析时自建 tokenizer 认领——官方解析内联 HTML
|
|
44
|
+
* 依赖 window.DOMParser,server 端没有 window,不认领会把它转义成字面文本
|
|
45
|
+
* (见 imageMarkdown.ts 头注)。
|
|
41
46
|
*/
|
|
42
47
|
export declare const pureImage: import('@tiptap/core').Node<import('@tiptap/extension-image').ImageOptions, any>;
|
|
43
48
|
export { pureChart, reportChart } from './chart/ChartExtension';
|
|
49
|
+
/**
|
|
50
|
+
* 段落 / 标题的 markdown 版本:带段落级样式(对齐 / 缩进 / 行高)时序列化为
|
|
51
|
+
* `<p style="…">` / `<hN style="…">`,无样式保持原生形式(见 paragraphMarkdown.ts)。
|
|
52
|
+
* StarterKit 里这两个节点要关掉,改用下面这份(同 CodeBlock / Image 的分层做法)。
|
|
53
|
+
*/
|
|
54
|
+
export declare const MarkdownParagraph: import('@tiptap/core').Node<import('@tiptap/extension-paragraph').ParagraphOptions, any>;
|
|
55
|
+
export declare const MarkdownHeading: import('@tiptap/core').Node<import('@tiptap/extension-heading').HeadingOptions, any>;
|
|
44
56
|
/**
|
|
45
57
|
* 三处共用的纯 schema 扩展——**不含 CodeBlock / Image / CitationRef / Chart**(见上方边界说明)。
|
|
46
58
|
* 编辑器:`[...baseExtensions, 增强CodeBlock, 增强Image, createCitationRef(), createChart(), Markdown]`
|